package org.mtmi.ui.controls;

import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.mtmi.tuio.listeners.DragListener;
import org.mtmi.tuio.listeners.SingleTapListener;
import org.mtmi.tuio.listeners.TUIODragEvent;
import org.mtmi.tuio.listeners.TUIOTapEvent;
import org.mtmi.ui.GraphicUtil;
import org.mtmi.ui.M;
import org.mtmi.ui.drawables.IDrawable;
import org.mtmi.ui.layouts.LayoutData;
import org.mtmi.ui.scenes.CLR;
import org.mtmi.ui.scenes.COLOR;
import org.mtmi.ui.scenes.FontManager;
import org.mtmi.ui.scenes.Scene;

/**
 * Given an input, {@link IContentProvider} provided elements which are
 * converted into image and/or text provided by {@link ILabelProvider}.
 * One and only one item (image/text pair) can be selected at a time,
 * user can choose to layout them horizontally or vertically ({@link #setHorizontal(boolean)}).
 * <p>
 * Choice should have 'limited' number of elements, because it display all of them
 * (if you need some Choice with hidden element and scroll bar, please investigate {@link ListView}).
 * <p>
 * Selection area can be opaque (draw at background of text/image), or transparent
 * in that case area is drawn above image/text, using a color or a gradient freely choosen
 * by user.
 * <p>
 * By default: selection text color and background are taken from {@link COLOR#ListSelectionForeground} 
 * and {@link COLOR#ListSelectionBackground}.
 *
 */
public class Choice extends AbstractRangeValueControl
  implements ITouchableControl, SingleTapListener, DragListener, IOrientable
{
    private static final int MG=8; //margin around texts
    private static final int AW=2*MG; //round rect arc width
    	
	private ILabelProvider labels;
	private IContentProvider contents;
	private Object input;
	
	private Color  c_text; //color of text.
	private Color  fg_selection, bg_selection; //color of selection.
	private int    bg_alpha;
	private boolean selection_use_gradient; //true if selection area is a gradient or flat.
	
	private boolean can_tap_to_select;
	private boolean display_vertical_separator;
	
	//layout and paint data:
	private IDrawable imgs[]; //computed by computeSize()
	private String texts[]; //computed by computeSize();
    private Point  sz_elements[]; //sizes of text including margin around image and/or text
    private Point  sz_imgs[]; //sizes of drawable area
    private Point  sz_texts[]; //sizes of text area
    private int    xci; //x center to draw images when layout is vertical
    private int    xlt; //x left to draw texts when layout is vertical
    private int    hi; //height / width of item, when layout is vertical/horizontal
    private int    wi; //width of item, when layout is vertical
    
    private float  valCurrent_shift; // extra value (0..1) to add to valCurrent to know where selection is currently sliding (0 normally)
    
    /**
     * @param scene Scene where this choice is created
     * @param flags M.HORIZONTAL | M.VERTICAL
     */
	public Choice( Scene scene, int flags) {
		super( scene, flags );
		setForeground( COLOR.grey100 );
		setBackground( COLOR.grey0 );
		c_text = COLOR.grey70 ;
		fg_selection = COLOR.ListSelectionForeground;
		bg_selection = COLOR.ListSelectionBackground;
		valCurrent_shift = 0;
		valMin = valMax = 0;
		selection_use_gradient = true;
		bg_alpha = 255;
		can_tap_to_select = true;
		display_vertical_separator=true;
	}
	
	/** @return current used label provider */
	public ILabelProvider getLabelProvider() { return labels; }
	/** Change label provider, note control is not refreshed */
	public void setLabelProvider( ILabelProvider lp ) { labels=lp; }
		
	/** @return current used content provider */
	public IContentProvider getContentProvider() { return contents; }
	/** Change contents provider, note control is not refreshed */
	public void setContentsProvider( IContentProvider cp ) { contents=cp; }
	
	/** @return current used input */
	public Object getInput() { return input; }
	/** Change current input, refresh control if there are label and content provider */
	public void setInput( Object o ) {
		input = o;
		refresh();
	}
	
	/** refresh the control, this method may change size of control */
	public void refresh()
	{
		setSize( computeSize( new LayoutData(scene) ) );
	}
	
	/** @return current selection policy, can tap, or must slide to select (default is true) */
	public boolean isCanTapToSelect() { return can_tap_to_select; }
	/** Change selection policy */
	public void setCanTapToSelect( boolean can ) { can_tap_to_select=can; }

	/** @return true if Choice display vertical separator when layout is horizontal (default is true) */
	public boolean isDisplayVerticalSeparator() { return display_vertical_separator; }
	/** Change display vertical separator property when layout is horizontal */
	public void setDisplayVerticalSeparator( boolean display ) { display_vertical_separator = display; }
	
	/** @return the color of selection background */
	public Color getSelectionBackground() { return bg_selection; }
	/** change the color of selection background, the control is not redrawn */
	public void setSelectionBackground( Color c ) { bg_selection=c; }
	
	/** @return the color of selection foreground */
	public Color getSelectionForeground() { return fg_selection; }
	/** change the color of selection background, the control is not redrawn */
	public void setSelectionForeground( Color c ) { fg_selection=c; }
	
	/** @return the color of unselected text */
	public Color getTextColor() { return c_text; }
	/** change the color of unselected text */
	public void setTextColor( Color c ) { c_text=c; }
	
	/** 
	 * Change the transparency of selection, when opaque selection is a background,
	 * otherwise selection is pain over items.
	 * @param a transparency from 0 (invisible) to 255 (opaque)
	 */
	public void setSelectionBackgroundAlpha( int alpha ) { bg_alpha=alpha; }
	/** @return the transparency used to display selection area*/
	public int  getSelectionBackgroundAlpha() { return bg_alpha; }
	
	/** @return true if selection uses gradient, false for flat color */
	public boolean isSelectionUsesGradient() { return selection_use_gradient; }
	/** Change selection area uses gradient property, does not redrawn control */
	public void setSelectionUsesGradient( boolean gradient ) { selection_use_gradient=gradient; }
	
	@Override
	public boolean isHorizontal() { 
		return (getFlags() & M.VERTICAL) == M.HORIZONTAL;
	}
	
	@Override
	/** Change Choice orientation, control is redrawn, and "rotate" around it center */
	public void setHorizontal( boolean horizontal ) {
		if( horizontal != isHorizontal() ) {
			
			redraw(); //mark old bounds are to be redraw
			
			int w2 = size.x/2;
			int h2 = size.y/2;
			int cx = location.x+w2;
			int cy = location.y+h2;
			
			if( horizontal ) {
				flags &= ~M.VERTICAL; //reset flag
			} else {
				flags |= M.VERTICAL; //set flag
			}
			refresh();
			setBounds( cx-size.x/2, cy-size.y/2, size.x, size.y );
			redraw();
		}
	}
	
	@Override
	public Point computeSize( LayoutData l )
	{
		//reset ...
		sz_elements = null;
		sz_imgs = null;
		sz_texts = null;
		texts = null;
		imgs = null;
		valMin = valMax = 0;
		
		if( contents==null || labels==null ) {
			return new Point( 30, 30 ); //default size
		}
		Object [] elements = contents.getElements( input );
		if( elements==null || elements.length==0 ) {
			return new Point( 30, 30 ); //default size
		}
		
		valMax = elements.length-1;
		
		int len = elements.length;
		sz_elements = new Point[ len ];
		sz_imgs = new Point[ len ];
		sz_texts = new Point[ len ];
		texts = new String[ len ];
		imgs = new IDrawable[ len ];
		
		l.gc.setFont( FontManager.getInstance().getNormalFont() );
		//1: get text height
		int htx = 0;
		int wtx = 0;
		for( int i=0; i<len; i++ ) {
			texts[i] = labels.getText( elements[i] );
			imgs [i] = labels.getImage( elements[i] );
			if( texts[i] !=null ) {
				sz_texts[ i ] = l.gc.textExtent( texts[i] );
				htx = Math.max( htx, sz_texts[i].y );
				wtx = Math.max( wtx, sz_texts[i].x );
			}
		}
		int isz = htx==0 ? 24 : htx; //choose size for proposed images size
		int hti = 0;
		int wti = 0;
		//2: get images sizes:
		for( int i=0; i<len; i++ ) {
			if( imgs[i]!=null ) {
				sz_imgs[i] = imgs[i].computeSize( isz, isz );
				hti = Math.max( hti, sz_imgs[i].y );
				wti = Math.max( wti, sz_imgs[i].x );
			}
		}
		//3: get width and height
		hi = MG+Math.max( htx, hti )+MG;
		int w, h;
		if( isHorizontal() ) {
			w = 0;
			for( int i=0; i<len; i++ ) {
				int we = 0;
				if( sz_imgs[i]!=null ) we += sz_imgs[i].x;
				if( sz_texts[i]!=null ) we += (we>0 ? MG : 0 ) + sz_texts[i].x;
//TODO: sz_elements can be int[] for width, height is known			
				sz_elements[i] = new Point( MG+we+MG, hi );
				w += sz_elements[i].x;
			}
			h=hi;
		} else {
			wi = MG+wti+MG+wtx+MG;
			w = wi;
			h = hi*len;
			xci = MG+wti/2;
			xlt = MG+wti+MG;
		}
		return new Point( borderWidth+w+borderWidth, borderWidth+h+borderWidth );
	}
	
	private boolean haveElements() {
		return (texts!=null && texts.length>0) || (imgs!=null && imgs.length>0 );
	}

	@Override
	public void paint(GC gc) 
	{
		int b2 = borderWidth/2;
		gc.setBackground( getBackground() );
		int aw = AW+borderWidth;
		GraphicUtil.fillRoundRectangle( gc, location.x+b2, location.y+b2, size.x-borderWidth, size.y-borderWidth, aw, aw );
		gc.setLineWidth( borderWidth );
		gc.setForeground( getForeground() );
		GraphicUtil.drawRoundRectangle( gc, location.x+b2, location.y+b2, size.x-borderWidth, size.y-borderWidth, aw, aw );
		
		if( !haveElements() ) return ;
		
		int x = location.x+borderWidth;
		int y = location.y+borderWidth;
		
		//paint selection at sliding location:
		// the rect is a "morph" from left text rect to right text rect
		int is = getValue();
		float rs = valCurrent_shift; //0..1
		Point s0 = sz_elements[is] ;
		
		int xs, ws, ys, hs;
		if( isHorizontal() ) {
			int dx=0;
			for( int i=0; i<is; i++ ) { 
				dx += sz_elements[i].x; 
			}
			if( is == sz_elements.length-1 ) {
				xs = x+dx;
				ws = s0.x;
			} else {
				Point s1 = sz_elements[is+1];
				xs = Math.round( x+dx+rs*s0.x );
				ws = Math.round( s0.x + rs*( s1.x-s0.x ) );
			}
			ys = y;
			hs = sz_elements[is].y;
		} else {
			//not rect size morphing in vertical layout
			xs = x;
			ws = wi;
			ys = Math.round( y + is*hi + rs*hi );
			hs = hi;
		}
		
		if( bg_alpha==255 ) {
			paintSelectionBackground( gc, xs, ys, ws, hs );
		}
		
		//display img/text above
		gc.setFont( FontManager.getInstance().getNormalFont() );
		int curr = Math.round(getValue()+valCurrent_shift);
		if( isHorizontal() ) {
			int xt = x+MG;
			int ht = sz_elements[0].y;
			for( int i=0; i<sz_elements.length; i++ ) {
				boolean is_curr = i == curr;
				//separate item by a vertical line:
				if( display_vertical_separator && i>0) {
					gc.setForeground( c_text );
					gc.setLineWidth( 0 );
					gc.drawLine( xt-MG, y+MG, xt-MG, y+ht-MG );
				}
				//image ?
				if( imgs[i]!=null ) {
					int st = is_curr ? IDrawable.SELECTED : IDrawable.NORMAL;
					imgs[i].paint( gc, xt, y+(ht-sz_imgs[i].y)/2, sz_imgs[i].x, sz_imgs[i].y, st );
					xt += sz_imgs[i].x+MG;
				}
				//text:
				if( texts[i]!=null ) {
					if( is_curr ) {
						gc.setForeground( fg_selection );
					} else {
						gc.setForeground( c_text );
					}
					gc.drawText( texts[i], xt, (y+ht-MG-sz_texts[i].y), true );
					xt += sz_texts[i].x+MG;
				}
				xt += MG; 
			}
		} else {
			int len = getItemCount();
			int yi = y;
			for( int i=0; i<len; i++ ) {
				boolean is_curr = i == curr;
				if( imgs[i] != null ) {
					int st = is_curr ? IDrawable.SELECTED : IDrawable.NORMAL;
					int ximg = x+xci-sz_imgs[i].x/2;
					int yimg = yi+(hi-sz_imgs[i].y)/2;
					imgs[i].paint( gc, ximg, yimg, sz_imgs[i].x, sz_imgs[i].y, st );
				}
				if( texts[i] != null ) {
					if( is_curr ) {
						gc.setForeground( fg_selection );
					} else {
						gc.setForeground( c_text );
					}
					int yt = yi + (hi-sz_texts[i].y)/2;
					gc.drawText( texts[i], x+xlt, yt, true );
				}
				yi += hi;
			}
		}
		
		if( bg_alpha!=255 ) {
			paintSelectionBackground( gc, xs, ys, ws, hs );
		}
	}
	
	private int getItemCount() {
		int count = 0;
		if( texts!=null ) count = texts.length;
		else if( imgs!=null ) count = imgs.length;
		return count;
	}
	
	protected void paintSelectionBackground( GC gc, int x, int y, int w, int h )
	{
		//simple: background:
		//gc.setBackground( bg_selection );
		//GraphicUtil.fillRoundRectangle( gc, xs, y, ws, sizes[is].y, AW, AW );
		
		//more complicated by fine !
		gc.setAlpha( bg_alpha );
		Path p = GraphicUtil.getRoundRectangle( gc, x, y, w, h, AW, AW );
		gc.setClipping( p );
		Color cl=null, cd=null;
		if( selection_use_gradient ) {
			cl = CLR.lighter( bg_selection, 0.5f );
			cd = CLR.darker( bg_selection, 0.5f );
			gc.setForeground( cl );
			gc.setBackground( cd );
		} else {
			gc.setBackground( bg_selection );
		}
		gc.fillGradientRectangle( x, y, w, h, true );
		gc.setClipping( (Rectangle)null );
		p.dispose();
		gc.setForeground( cd );
		//gc.setForeground( COLOR.ListSelectionBackground );
		gc.setLineWidth( 0 );
		GraphicUtil.drawRoundRectangle( gc, x, y, w-1, h-1, AW, AW );
		if( cl!= null ) cl.dispose();
		if( cd!=null ) cd.dispose();
		gc.setAlpha( 255 );
	}

	@Override
	public boolean containsTouch(TouchControlEvent e) {
		return GraphicUtil.rectContains( location, size, e.x, e.y );
	}

	private int state=0; //0: none: 1:sliding
	private int msdx, msdy; //catch dist from top left selection
	private int save_selected;
	
	@Override
	public void tapDown( TUIOTapEvent event )
	{
		if( !haveElements() ) return ; //no need to slide
		state=1;
		int valCurrent = getValue();
		save_selected = valCurrent;
		if( isHorizontal() ) {
			int xt=location.x+borderWidth;
			for( int i=0; i<sz_elements.length; i++ ) { 
				if( event.ex < xt + sz_elements[i].x ) {
					//mouse if over element #i;
					if( valCurrent!=i ) {
						if( can_tap_to_select ) {
							valCurrent = i;
							redraw();
						} else {
							//must slide : but user doesn't on selection.
							state=0;
							return;
						}
					}
					break; //found
				}
				xt += sz_elements[i].x; 
			}
			msdx = xt-event.ex;
		} else {
			int i = (event.ey-location.y-borderWidth)/hi;
			if( valCurrent!=i ) {
				//must select "thumb"
				if( can_tap_to_select ) {
					valCurrent = i;
					redraw();
				} else {
					//must slide : but user doesn't on selection.
					state=0;
					return;
				}
			}
			msdy = location.y+i*hi - event.ey;
		}
	}

	@Override
	public void tapUp( TUIOTapEvent event ) {
		if( state==1 ) {
			//time to set selected index to int value:
			int is = Math.round( getValue()+valCurrent_shift );
			setValue( is );
			valCurrent_shift = 0;
			redraw();
			if( is != save_selected ) {
				notifyValueChanged();
			}
		}
	}

	@Override
	public void drag( TUIODragEvent event )
	{
		if( state == 1 ) {
			int count = getItemCount();
			if( isHorizontal() ) {
				int nx = msdx+event.ex;
				//convert to selection value:
				int xt=location.x+borderWidth;
				if( nx <= xt ) {
					valCurrent_shift=0;
				}
				else {
					for( int i=0; i<count; i++ ) { 
						int lx = xt + sz_elements[i].x;
						if( nx <= lx ) {
							int dx = nx-xt;
							float fx = dx / (float)sz_elements[i].x;
							if( fx > 1 ) fx = 1;
							valCurrent_shift = i+fx;
							break;
						}
						xt = lx;
					}
				}
			}
			else {
				int ny = msdy + event.ey;
				int yi = location.y+borderWidth;
				if( ny <= yi ) {
					valCurrent_shift=0;
				} 
				else {
					int i = (ny-location.y-borderWidth)/hi;
					yi += i*hi;
					int dy = ny - yi;
					float fy = dy / (float)hi;
					if( fy > 1 ) fy = 1;
					valCurrent_shift = i+fy;
				}
			}
			valCurrent_shift = Math.max( 0, Math.min( valCurrent_shift, count-1 ) );
			setValue( (int)valCurrent_shift );
			valCurrent_shift -= getValue(); //must be 0..1 by contract
			redraw();
		}
	}

	

}
